import json
import logging
import random
import re
import time
from collections import Counter
from typing import Union

from browsermobproxy import Server
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.by import By

import util
from const import CODE2VAL, DEV
from const import cfg
from util import msg_send_utils
from util.mirai_utils import get_sender_id, get_group_id
from util.msg_send_utils import send_group_msg, send_friend_msg

logger = logging.getLogger()


class SingleMsgHandlerBase(object):

    def can_handle(self, msg: dict, *args, **kwargs):
        raise NotImplementedError

    def handle(self, msg: dict, ctx, *args, **kwargs):
        raise NotImplementedError


class ChatGPTMsgHandler(SingleMsgHandlerBase):
    """
    ChatGPT相关消息处理
    """

    def __init__(self):

        # Start the Browsermob Proxy Server
        # self.server = Server(
        #     r"D:\browsermob-proxy-2.1.4-bin\browsermob-proxy-2.1.4\bin\browsermob-proxy",
        #     options={"port": 8545})
        # self.server.start()
        # proxy = self.server.create_proxy()

        chrome_options = webdriver.ChromeOptions()
        # chrome_options.add_argument('--proxy-server={0}'.format(proxy.proxy))
        chrome_options.add_argument('--ignore-certificate-errors')
        chrome_options.add_argument("--disable-blink-features=AutomationControlled")
        driver = webdriver.Chrome(options=chrome_options)
        self.driver = driver

        self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
            "source": """
                Object.defineProperty(navigator,'webdriver',{
                    get: () => undefined
                })
            """
        })
        # Open a webpage using Selenium
        self.driver.get("https://chat.openai.com/chat")
        # 手动完成登录

        # proxy.new_har("chatGPT", options={
        #     'captureContent': True,
        #     'captureHeaders': True
        # })
        # har = proxy.har

    def close(self):
        # self.server.stop()
        self.driver.quit()

    def can_handle(self, msg: dict, *args, **kwargs):
        msg_type = msg.get('type')
        if msg_type == "FriendMessage":
            return True

        check_type = msg_type in ["FriendMessage", "GroupMessage"]
        check_at = msg["messageChain"][1]["type"] == "At" and str(msg["messageChain"][1]["target"]) == cfg["qq"]
        return check_type and check_at

    def handle(self, msg: dict, ctx, *args, **kwargs):
        try:
            msg_chain = msg['messageChain']
            # Source类型永远为chain的第一个元素: {'type': 'Source', 'id': 26675, 'time': 1661589588}
            source_msg = msg_chain[0]
            logger.info(source_msg)
            for single_msg in msg_chain:
                single_msg_type = single_msg['type']
                if single_msg_type == 'Plain':
                    content: str = single_msg['text'].lower().strip()
                    cfg = ctx.cfg
                    if content == "刷新":
                        self.driver.refresh()
                    else:
                        self.driver.find_element(By.CSS_SELECTOR, "textarea").send_keys(content)
                        self.driver.find_element(By.CSS_SELECTOR, "button.absolute").click()
                        pre = None
                        next = "dd"
                        while (next != pre and next != "") or len(next.strip()) < 4:
                            time.sleep(2)
                            soup = BeautifulSoup(self.driver.page_source)
                            reply = list(soup.select("main > div.flex-1.overflow-hidden > div > div > div")[-1].children)[-2]
                            pre = next
                            next = reply.text.replace(content, "") or str(time.time())

                        if len(next) > 800:
                            self.send_msg(cfg, msg, next[:800])
                            self.send_msg(cfg, msg, next[800:])
                        else:
                            self.send_msg(cfg, msg, next)


        except Exception as e:
            logger.exception(e)

    def send_msg(self, cfg, msg, next):
        msg_chain = [
            {"type": "Plain", "text": next},
        ]
        if msg["type"] == "FriendMessage":
            msg_send_utils.send_friend_msg(cfg["session"],
                                           get_sender_id(msg),
                                           msg_chain)
        else:
            send_group_msg(cfg["session"],
                           get_group_id(msg),
                           msg_chain)

    def send_success(self, cfg, msg, ind, status):
        self.send_msg(cfg, msg, f"[SUCCESS] 第 {ind} 题设置为：{status}")


class LeetCodeMsgHandler(SingleMsgHandlerBase):
    """
    力扣相关消息处理
    """

    def can_handle(self, msg: dict, *args, **kwargs):
        msg_type = msg.get('type')
        return msg_type in ["FriendMessage", "GroupMessage"]

    def handle(self, msg: dict, ctx, *args, **kwargs):
        try:
            msg_chain = msg['messageChain']
            # Source类型永远为chain的第一个元素: {'type': 'Source', 'id': 26675, 'time': 1661589588}
            source_msg = msg_chain[0]
            logger.info(source_msg)
            for single_msg in msg_chain:
                single_msg_type = single_msg['type']
                if single_msg_type == 'Plain':
                    content: str = single_msg['text'].lower()
                    cfg = ctx.cfg
                    if content in ['力扣', '每日一题', 'leetcode']:
                        # msg_chain = [
                        #     {"type": "Plain", "text": "稍等， 正在获取..."}
                        # ]
                        # if msg["type"] == "FriendMessage":
                        #     msg_send_utils.send_friend_msg(cfg["session"], get_sender_id(msg), msg_chain)
                        # else:
                        #     send_group_msg(cfg["session"], get_group_id(msg), msg_chain)
                        img_path, problem = util.get_problem_png_path()
                        logger.info('找到关键词，发送图片')
                        msg_chain = [
                            {"type": "Plain", "text": "第 " + problem["ind"] + " 题"},
                            {"type": "Plain", "text": ": " + problem["title"]},
                            # {"type": "Plain", "text": "\n标签: " + json.dumps(problem["tags"], ensure_ascii=False)},
                            {"type": "Image", "path": img_path}
                        ]
                        if msg["type"] == "FriendMessage":
                            msg_send_utils.send_friend_msg(cfg["session"], get_sender_id(msg), msg_chain)
                        else:
                            send_group_msg(cfg["session"], get_group_id(msg), msg_chain)
                        continue

                    if content.startswith("已解决") or content.startswith("done"):
                        _, ind = content.split(" ")
                        ind = int(ind)
                        util.solved_problems(ind)
                        self.send_success(cfg, msg, ind, "已解决")

                    elif content.startswith("未解决") or content.startswith("undone"):
                        _, ind = content.split(" ")
                        ind = int(ind)
                        util.unsolved_problems(ind)
                        self.send_success(cfg, msg, ind, "未解决")
        except Exception as e:
            logger.exception(e)

    def send_success(self, cfg, msg, ind, status):
        msg_chain = [
            {"type": "Plain", "text": f"[SUCCESS] 第 {ind} 题设置为：{status}"},
        ]
        if msg["type"] == "FriendMessage":
            msg_send_utils.send_friend_msg(cfg["session"],
                                           get_sender_id(msg),
                                           msg_chain)
        else:
            send_group_msg(cfg["session"],
                           get_group_id(msg),
                           msg_chain)


class RedisCommandMsgHandler(SingleMsgHandlerBase):
    """
    redis 命令相关处理
    """

    ALL_COMMANDS = ['commands', 'acl cat', 'acl deluser', 'acl dryrun', 'acl genpass', 'acl getuser', 'acl list',
                    'acl load', 'acl log', 'acl save', 'acl setuser', 'acl users', 'acl whoami', 'append', 'asking',
                    'auth', 'bf.add', 'bf.exists', 'bf.info', 'bf.insert', 'bf.loadchunk', 'bf.madd', 'bf.mexists',
                    'bf.reserve', 'bf.scandump', 'bgrewriteaof', 'bgsave', 'bitcount', 'bitfield', 'bitfield_ro',
                    'bitop', 'bitpos', 'blmove', 'blmpop', 'blpop', 'brpop', 'brpoplpush', 'bzmpop', 'bzpopmax',
                    'bzpopmin', 'cf.add', 'cf.addnx', 'cf.count', 'cf.del', 'cf.exists', 'cf.info', 'cf.insert',
                    'cf.insertnx', 'cf.loadchunk', 'cf.mexists', 'cf.reserve', 'cf.scandump', 'client caching',
                    'client getname', 'client getredir', 'client id', 'client info', 'client kill', 'client list',
                    'client no evict', 'client pause', 'client reply', 'client setname', 'client tracking',
                    'client trackinginfo', 'client unblock', 'client unpause', 'cluster addslots',
                    'cluster addslotsrange', 'cluster bumpepoch', 'cluster count failure reports',
                    'cluster countkeysinslot', 'cluster delslots', 'cluster delslotsrange', 'cluster failover',
                    'cluster flushslots', 'cluster forget', 'cluster getkeysinslot', 'cluster info', 'cluster keyslot',
                    'cluster links', 'cluster meet', 'cluster myid', 'cluster nodes', 'cluster replicas',
                    'cluster replicate', 'cluster reset', 'cluster saveconfig', 'cluster set config epoch',
                    'cluster setslot', 'cluster shards', 'cluster slaves', 'cluster slots', 'cms.incrby', 'cms.info',
                    'cms.initbydim', 'cms.initbyprob', 'cms.merge', 'cms.query', 'command', 'command count',
                    'command docs', 'command getkeys', 'command getkeysandflags', 'command info', 'command list',
                    'config get', 'config resetstat', 'config rewrite', 'config set', 'copy', 'dbsize', 'decr',
                    'decrby', 'del', 'discard', 'dump', 'echo', 'eval', 'eval_ro', 'evalsha', 'evalsha_ro', 'exec',
                    'exists', 'expire', 'expireat', 'expiretime', 'failover', 'fcall', 'fcall_ro', 'flushall',
                    'flushdb', 'ft._list', 'ft.aggregate', 'ft.aliasadd', 'ft.aliasdel', 'ft.aliasupdate', 'ft.alter',
                    'ft.config get', 'ft.config set', 'ft.create', 'ft.cursor del', 'ft.cursor read', 'ft.dictadd',
                    'ft.dictdel', 'ft.dictdump', 'ft.dropindex', 'ft.explain', 'ft.explaincli', 'ft.info', 'ft.profile',
                    'ft.search', 'ft.spellcheck', 'ft.sugadd', 'ft.sugdel', 'ft.sugget', 'ft.suglen', 'ft.syndump',
                    'ft.synupdate', 'ft.tagvals', 'function delete', 'function dump', 'function flush', 'function kill',
                    'function list', 'function load', 'function restore', 'function stats', 'geoadd', 'geodist',
                    'geohash', 'geopos', 'georadius', 'georadius_ro', 'georadiusbymember', 'georadiusbymember_ro',
                    'geosearch', 'geosearchstore', 'get', 'getbit', 'getdel', 'getex', 'getrange', 'getset',
                    'graph.config get', 'graph.config set', 'graph.delete', 'graph.explain', 'graph.list',
                    'graph.profile', 'graph.query', 'graph.ro_query', 'graph.slowlog', 'hdel', 'hello', 'hexists',
                    'hget', 'hgetall', 'hincrby', 'hincrbyfloat', 'hkeys', 'hlen', 'hmget', 'hmset', 'hrandfield',
                    'hscan', 'hset', 'hsetnx', 'hstrlen', 'hvals', 'incr', 'incrby', 'incrbyfloat', 'info',
                    'json.arrappend', 'json.arrindex', 'json.arrinsert', 'json.arrlen', 'json.arrpop', 'json.arrtrim',
                    'json.clear', 'json.debug', 'json.debug memory', 'json.del', 'json.forget', 'json.get', 'json.mget',
                    'json.numincrby', 'json.nummultby', 'json.objkeys', 'json.objlen', 'json.resp', 'json.set',
                    'json.strappend', 'json.strlen', 'json.toggle', 'json.type', 'keys', 'lastsave', 'latency doctor',
                    'latency graph', 'latency histogram', 'latency history', 'latency latest', 'latency reset', 'lcs',
                    'lindex', 'linsert', 'llen', 'lmove', 'lmpop', 'lolwut', 'lpop', 'lpos', 'lpush', 'lpushx',
                    'lrange', 'lrem', 'lset', 'ltrim', 'memory doctor', 'memory malloc stats', 'memory purge',
                    'memory stats', 'memory usage', 'mget', 'migrate', 'module list', 'module load', 'module loadex',
                    'module unload', 'monitor', 'move', 'mset', 'msetnx', 'multi', 'object encoding', 'object freq',
                    'object idletime', 'object refcount', 'persist', 'pexpire', 'pexpireat', 'pexpiretime', 'pfadd',
                    'pfcount', 'pfdebug', 'pfmerge', 'pfselftest', 'ping', 'psetex', 'psubscribe', 'psync', 'pttl',
                    'publish', 'pubsub channels', 'pubsub numpat', 'pubsub numsub', 'pubsub shardchannels',
                    'pubsub shardnumsub', 'punsubscribe', 'quit', 'randomkey', 'readonly', 'readwrite', 'rename',
                    'renamenx', 'replconf', 'replicaof', 'reset', 'restore', 'restore asking', 'role', 'rpop',
                    'rpoplpush', 'rpush', 'rpushx', 'sadd', 'save', 'scan', 'scard', 'script debug', 'script exists',
                    'script flush', 'script kill', 'script load', 'sdiff', 'sdiffstore', 'select', 'set', 'setbit',
                    'setex', 'setnx', 'setrange', 'shutdown', 'sinter', 'sintercard', 'sinterstore', 'sismember',
                    'slaveof', 'slowlog get', 'slowlog len', 'slowlog reset', 'smembers', 'smismember', 'smove', 'sort',
                    'sort_ro', 'spop', 'spublish', 'srandmember', 'srem', 'sscan', 'ssubscribe', 'strlen', 'subscribe',
                    'substr', 'sunion', 'sunionstore', 'sunsubscribe', 'swapdb', 'sync', 'tdigest.add',
                    'tdigest.byrank', 'tdigest.byrevrank', 'tdigest.cdf', 'tdigest.create', 'tdigest.info',
                    'tdigest.max', 'tdigest.merge', 'tdigest.min', 'tdigest.quantile', 'tdigest.rank', 'tdigest.reset',
                    'tdigest.revrank', 'tdigest.trimmed_mean', 'time', 'topk.add', 'topk.count', 'topk.incrby',
                    'topk.info', 'topk.list', 'topk.query', 'topk.reserve', 'touch', 'ts.add', 'ts.alter', 'ts.create',
                    'ts.createrule', 'ts.decrby', 'ts.del', 'ts.deleterule', 'ts.get', 'ts.incrby', 'ts.info',
                    'ts.madd', 'ts.mget', 'ts.mrange', 'ts.mrevrange', 'ts.queryindex', 'ts.range', 'ts.revrange',
                    'ttl', 'type', 'unlink', 'unsubscribe', 'unwatch', 'wait', 'watch', 'xack', 'xadd', 'xautoclaim',
                    'xclaim', 'xdel', 'xgroup create', 'xgroup createconsumer', 'xgroup delconsumer', 'xgroup destroy',
                    'xgroup setid', 'xinfo consumers', 'xinfo groups', 'xinfo stream', 'xlen', 'xpending', 'xrange',
                    'xread', 'xreadgroup', 'xrevrange', 'xsetid', 'xtrim', 'zadd', 'zcard', 'zcount', 'zdiff',
                    'zdiffstore', 'zincrby', 'zinter', 'zintercard', 'zinterstore', 'zlexcount', 'zmpop', 'zmscore',
                    'zpopmax', 'zpopmin', 'zrandmember', 'zrange', 'zrangebylex', 'zrangebyscore', 'zrangestore',
                    'zrank', 'zrem', 'zremrangebylex', 'zremrangebyrank', 'zremrangebyscore', 'zrevrange',
                    'zrevrangebylex', 'zrevrangebyscore', 'zrevrank', 'zscan', 'zscore', 'zunion', 'zunionstore']
    CMD2DESC = json.loads(
        '{"ACL CAT": "List the ACL categories or the commands inside a category", "ACL DELUSER": "Remove the specified ACL users and the associated rules", "ACL DRYRUN": "Returns whether the user can execute the given command without executing the command.", "ACL GENPASS": "Generate a pseudorandom secure password to use for ACL users", "ACL GETUSER": "Get the rules for a specific ACL user", "ACL LIST": "List the current ACL rules in ACL config file format", "ACL LOAD": "Reload the ACLs from the configured ACL file", "ACL LOG": "List latest events denied because of ACLs in place", "ACL SAVE": "Save the current ACL rules in the configured ACL file", "ACL SETUSER": "Modify or create the rules for a specific ACL user", "ACL USERS": "List the username of all the configured ACL rules", "ACL WHOAMI": "Return the name of the user associated to the current connection", "APPEND": "Append a value to a key", "ASKING": "Sent by cluster clients after an -ASK redirect", "AUTH": "Authenticate to the server", "BF.ADD": "Adds an item to a Bloom Filter", "BF.EXISTS": "Checks whether an item exists in a Bloom Filter", "BF.INFO": "Returns information about a Bloom Filter", "BF.INSERT": "Adds one or more items to a Bloom Filter. A filter will be created if it does not exist", "BF.LOADCHUNK": "Restores a filter previously saved using SCANDUMP", "BF.MADD": "Adds one or more items to a Bloom Filter. A filter will be created if it does not exist", "BF.MEXISTS": "Checks whether one or more items exist in a Bloom Filter", "BF.RESERVE": "Creates a new Bloom Filter", "BF.SCANDUMP": "Begins an incremental save of the bloom filter", "BGREWRITEAOF": "Asynchronously rewrite the append-only file", "BGSAVE": "Asynchronously save the dataset to disk", "BITCOUNT": "Count set bits in a string", "BITFIELD": "Perform arbitrary bitfield integer operations on strings", "BITFIELD_RO": "Perform arbitrary bitfield integer operations on strings. Read-only variant of BITFIELD", "BITOP": "Perform bitwise operations between strings", "BITPOS": "Find first bit set or clear in a string", "BLMOVE": "Pop an element from a list, push it to another list and return it; or block until one is available", "BLMPOP": "Pop elements from a list, or block until one is available", "BLPOP": "Remove and get the first element in a list, or block until one is available", "BRPOP": "Remove and get the last element in a list, or block until one is available", "BRPOPLPUSH": "Pop an element from a list, push it to another list and return it; or block until one is available", "BZMPOP": "Remove and return members with scores in a sorted set or block until one is available", "BZPOPMAX": "Remove and return the member with the highest score from one or more sorted sets, or block until one is available", "BZPOPMIN": "Remove and return the member with the lowest score from one or more sorted sets, or block until one is available", "CF.ADD": "Adds an item to a Cuckoo Filter", "CF.ADDNX": "Adds an item to a Cuckoo Filter if the item did not exist previously.", "CF.COUNT": "Return the number of times an item might be in a Cuckoo Filter", "CF.DEL": "Deletes an item from a Cuckoo Filter", "CF.EXISTS": "Checks whether one or more items exist in a Cuckoo Filter", "CF.INFO": "Returns information about a Cuckoo Filter", "CF.INSERT": "Adds one or more items to a Cuckoo Filter. A filter will be created if it does not exist", "CF.INSERTNX": "Adds one or more items to a Cuckoo Filter if the items did not exist previously. A filter will be created if it does not exist", "CF.LOADCHUNK": "Restores a filter previously saved using SCANDUMP", "CF.MEXISTS": "Checks whether one or more items exist in a Cuckoo Filter", "CF.RESERVE": "Creates a new Cuckoo Filter", "CF.SCANDUMP": "Begins an incremental save of the bloom filter", "CLIENT CACHING": "Instruct the server about tracking or not keys in the next request", "CLIENT GETNAME": "Get the current connection name", "CLIENT GETREDIR": "Get tracking notifications redirection client ID if any", "CLIENT ID": "Returns the client ID for the current connection", "CLIENT INFO": "Returns information about the current client connection.", "CLIENT KILL": "Kill the connection of a client", "CLIENT LIST": "Get the list of client connections", "CLIENT NO-EVICT": "Set client eviction mode for the current connection", "CLIENT PAUSE": "Stop processing commands from clients for some time", "CLIENT REPLY": "Instruct the server whether to reply to commands", "CLIENT SETNAME": "Set the current connection name", "CLIENT TRACKING": "Enable or disable server assisted client side caching support", "CLIENT TRACKINGINFO": "Return information about server assisted client side caching for the current connection", "CLIENT UNBLOCK": "Unblock a client blocked in a blocking command from a different connection", "CLIENT UNPAUSE": "Resume processing of clients that were paused", "CLUSTER ADDSLOTS": "Assign new hash slots to receiving node", "CLUSTER ADDSLOTSRANGE": "Assign new hash slots to receiving node", "CLUSTER BUMPEPOCH": "Advance the cluster config epoch", "CLUSTER COUNT-FAILURE-REPORTS": "Return the number of failure reports active for a given node", "CLUSTER COUNTKEYSINSLOT": "Return the number of local keys in the specified hash slot", "CLUSTER DELSLOTS": "Set hash slots as unbound in receiving node", "CLUSTER DELSLOTSRANGE": "Set hash slots as unbound in receiving node", "CLUSTER FAILOVER": "Forces a replica to perform a manual failover of its master.", "CLUSTER FLUSHSLOTS": "Delete a node\'s own slots information", "CLUSTER FORGET": "Remove a node from the nodes table", "CLUSTER GETKEYSINSLOT": "Return local key names in the specified hash slot", "CLUSTER INFO": "Provides info about Redis Cluster node state", "CLUSTER KEYSLOT": "Returns the hash slot of the specified key", "CLUSTER LINKS": "Returns a list of all TCP links to and from peer nodes in cluster", "CLUSTER MEET": "Force a node cluster to handshake with another node", "CLUSTER MYID": "Return the node id", "CLUSTER NODES": "Get Cluster config for the node", "CLUSTER REPLICAS": "List replica nodes of the specified master node", "CLUSTER REPLICATE": "Reconfigure a node as a replica of the specified master node", "CLUSTER RESET": "Reset a Redis Cluster node", "CLUSTER SAVECONFIG": "Forces the node to save cluster state on disk", "CLUSTER SET-CONFIG-EPOCH": "Set the configuration epoch in a new node", "CLUSTER SETSLOT": "Bind a hash slot to a specific node", "CLUSTER SHARDS": "Get array of cluster slots to node mappings", "CLUSTER SLAVES": "List replica nodes of the specified master node", "CLUSTER SLOTS": "Get array of Cluster slot to node mappings", "CMS.INCRBY": "Increases the count of one or more items by increment", "CMS.INFO": "Returns information about a sketch", "CMS.INITBYDIM": "Initializes a Count-Min Sketch to dimensions specified by user", "CMS.INITBYPROB": "Initializes a Count-Min Sketch to accommodate requested tolerances.", "CMS.MERGE": "Merges several sketches into one sketch", "CMS.QUERY": "Returns the count for one or more items in a sketch", "COMMAND": "Get array of Redis command details", "COMMAND COUNT": "Get total number of Redis commands", "COMMAND DOCS": "Get array of specific Redis command documentation", "COMMAND GETKEYS": "Extract keys given a full Redis command", "COMMAND GETKEYSANDFLAGS": "Extract keys and access flags given a full Redis command", "COMMAND INFO": "Get array of specific Redis command details, or all when no argument is given.", "COMMAND LIST": "Get an array of Redis command names", "CONFIG GET": "Get the values of configuration parameters", "CONFIG RESETSTAT": "Reset the stats returned by INFO", "CONFIG REWRITE": "Rewrite the configuration file with the in memory configuration", "CONFIG SET": "Set configuration parameters to the given values", "COPY": "Copy a key", "DBSIZE": "Return the number of keys in the selected database", "DECR": "Decrement the integer value of a key by one", "DECRBY": "Decrement the integer value of a key by the given number", "DEL": "Delete a key", "DISCARD": "Discard all commands issued after MULTI", "DUMP": "Return a serialized version of the value stored at the specified key.", "ECHO": "Echo the given string", "EVAL": "Execute a Lua script server side", "EVAL_RO": "Execute a read-only Lua script server side", "EVALSHA": "Execute a Lua script server side", "EVALSHA_RO": "Execute a read-only Lua script server side", "EXEC": "Execute all commands issued after MULTI", "EXISTS": "Determine if a key exists", "EXPIRE": "Set a key\'s time to live in seconds", "EXPIREAT": "Set the expiration for a key as a UNIX timestamp", "EXPIRETIME": "Get the expiration Unix timestamp for a key", "FAILOVER": "Start a coordinated failover between this server and one of its replicas.", "FCALL": "Invoke a function", "FCALL_RO": "Invoke a read-only function", "FLUSHALL": "Remove all keys from all databases", "FLUSHDB": "Remove all keys from the current database", "FT._LIST": "Returns a list of all existing indexes", "FT.AGGREGATE": "Adds terms to a dictionary", "FT.ALIASADD": "Adds an alias to the index", "FT.ALIASDEL": "Deletes an alias from the index", "FT.ALIASUPDATE": "Adds or updates an alias to the index", "FT.ALTER": "Adds a new field to the index", "FT.CONFIG GET": "Retrieves runtime configuration options", "FT.CONFIG SET": "Sets runtime configuration options", "FT.CREATE": "Creates an index with the given spec", "FT.CURSOR DEL": "Deletes a cursor", "FT.CURSOR READ": "Reads from a cursor", "FT.DICTADD": "Adds terms to a dictionary", "FT.DICTDEL": "Deletes terms from a dictionary", "FT.DICTDUMP": "Dumps all terms in the given dictionary", "FT.DROPINDEX": "Deletes the index", "FT.EXPLAIN": "Returns the execution plan for a complex query", "FT.EXPLAINCLI": "Returns the execution plan for a complex query", "FT.INFO": "Returns information and statistics on the index", "FT.PROFILE": "Performs a `FT.SEARCH` or `FT.AGGREGATE` command and collects performance information", "FT.SEARCH": "Searches the index with a textual query, returning either documents or just ids", "FT.SPELLCHECK": "Performs spelling correction on a query, returning suggestions for misspelled terms", "FT.SUGADD": "Adds a suggestion string to an auto-complete suggestion dictionary", "FT.SUGDEL": "Deletes a string from a suggestion index", "FT.SUGGET": "Gets completion suggestions for a prefix", "FT.SUGLEN": "Gets the size of an auto-complete suggestion dictionary", "FT.SYNDUMP": "Dumps the contents of a synonym group", "FT.SYNUPDATE": "Creates or updates a synonym group with additional terms", "FT.TAGVALS": "Returns the distinct tags indexed in a Tag field", "FUNCTION DELETE": "Delete a function by name", "FUNCTION DUMP": "Dump all functions into a serialized binary payload", "FUNCTION FLUSH": "Deleting all functions", "FUNCTION KILL": "Kill the function currently in execution.", "FUNCTION LIST": "List information about all the functions", "FUNCTION LOAD": "Create a function with the given arguments (name, code, description)", "FUNCTION RESTORE": "Restore all the functions on the given payload", "FUNCTION STATS": "Return information about the function currently running (name, description, duration)", "GEOADD": "Add one or more geospatial items in the geospatial index represented using a sorted set", "GEODIST": "Returns the distance between two members of a geospatial index", "GEOHASH": "Returns members of a geospatial index as standard geohash strings", "GEOPOS": "Returns longitude and latitude of members of a geospatial index", "GEORADIUS": "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point", "GEORADIUS_RO": "A read-only variant for GEORADIUS", "GEORADIUSBYMEMBER": "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member", "GEORADIUSBYMEMBER_RO": "A read-only variant for GEORADIUSBYMEMBER", "GEOSEARCH": "Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle.", "GEOSEARCHSTORE": "Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle, and store the result in another key.", "GET": "Get the value of a key", "GETBIT": "Returns the bit value at offset in the string value stored at key", "GETDEL": "Get the value of a key and delete the key", "GETEX": "Get the value of a key and optionally set its expiration", "GETRANGE": "Get a substring of the string stored at a key", "GETSET": "Set the string value of a key and return its old value", "GRAPH.CONFIG GET": "Retrieves a RedisGraph configuration", "GRAPH.CONFIG SET": "Updates a RedisGraph configuration", "GRAPH.DELETE": "Completely removes the graph and all of its entities", "GRAPH.EXPLAIN": "Returns a query execution plan without running the query", "GRAPH.LIST": "Lists all graph keys in the keyspace", "GRAPH.PROFILE": "Executes a query and returns an execution plan augmented with metrics for each operation\'s execution", "GRAPH.QUERY": "Executes the given query against a specified graph", "GRAPH.RO_QUERY": "Executes a given read only query against a specified graph", "GRAPH.SLOWLOG": "Returns a list containing up to 10 of the slowest queries issued against the given graph", "HDEL": "Delete one or more hash fields", "HELLO": "Handshake with Redis", "HEXISTS": "Determine if a hash field exists", "HGET": "Get the value of a hash field", "HGETALL": "Get all the fields and values in a hash", "HINCRBY": "Increment the integer value of a hash field by the given number", "HINCRBYFLOAT": "Increment the float value of a hash field by the given amount", "HKEYS": "Get all the fields in a hash", "HLEN": "Get the number of fields in a hash", "HMGET": "Get the values of all the given hash fields", "HMSET": "Set multiple hash fields to multiple values", "HRANDFIELD": "Get one or multiple random fields from a hash", "HSCAN": "Incrementally iterate hash fields and associated values", "HSET": "Set the string value of a hash field", "HSETNX": "Set the value of a hash field, only if the field does not exist", "HSTRLEN": "Get the length of the value of a hash field", "HVALS": "Get all the values in a hash", "INCR": "Increment the integer value of a key by one", "INCRBY": "Increment the integer value of a key by the given amount", "INCRBYFLOAT": "Increment the float value of a key by the given amount", "INFO": "Get information and statistics about the server", "JSON.ARRAPPEND": "Append one or more json values into the array at path after the last element in it.", "JSON.ARRINDEX": "Returns the index of the first occurrence of a JSON scalar value in the array at path", "JSON.ARRINSERT": "Inserts the JSON scalar(s) value at the specified index in the array at path", "JSON.ARRLEN": "Returns the length of the array at path", "JSON.ARRPOP": "Removes and returns the element at the specified index in the array at path", "JSON.ARRTRIM": "Trims the array at path to contain only the specified inclusive range of indices from start to stop", "JSON.CLEAR": "Clears all values from an array or an object and sets numeric values to `0`", "JSON.DEBUG": "Debugging container command", "JSON.DEBUG MEMORY": "Reports the size in bytes of a key", "JSON.DEL": "Deletes a value", "JSON.FORGET": "Deletes a value", "JSON.GET": "Gets the value at one or more paths in JSON serialized form", "JSON.MGET": "Returns the values at a path from one or more keys", "JSON.NUMINCRBY": "Increments the numeric value at path by a value", "JSON.NUMMULTBY": "Multiplies the numeric value at path by a value", "JSON.OBJKEYS": "Returns the JSON keys of the object at path", "JSON.OBJLEN": "Returns the number of keys of the object at path", "JSON.RESP": "Returns the JSON value at path in Redis Serialization Protocol (RESP)", "JSON.SET": "Sets or updates the JSON value at a path", "JSON.STRAPPEND": "Appends a string to a JSON string value at path", "JSON.STRLEN": "Returns the length of the JSON String at path in key", "JSON.TOGGLE": "Toggles a boolean value", "JSON.TYPE": "Returns the type of the JSON value at path", "KEYS": "Find all keys matching the given pattern", "LASTSAVE": "Get the UNIX time stamp of the last successful save to disk", "LATENCY DOCTOR": "Return a human readable latency analysis report.", "LATENCY GRAPH": "Return a latency graph for the event.", "LATENCY HISTOGRAM": "Return the cumulative distribution of latencies of a subset of commands or all.", "LATENCY HISTORY": "Return timestamp-latency samples for the event.", "LATENCY LATEST": "Return the latest latency samples for all events.", "LATENCY RESET": "Reset latency data for one or more events.", "LCS": "Find longest common substring", "LINDEX": "Get an element from a list by its index", "LINSERT": "Insert an element before or after another element in a list", "LLEN": "Get the length of a list", "LMOVE": "Pop an element from a list, push it to another list and return it", "LMPOP": "Pop elements from a list", "LOLWUT": "Display some computer art and the Redis version", "LPOP": "Remove and get the first elements in a list", "LPOS": "Return the index of matching elements on a list", "LPUSH": "Prepend one or multiple elements to a list", "LPUSHX": "Prepend an element to a list, only if the list exists", "LRANGE": "Get a range of elements from a list", "LREM": "Remove elements from a list", "LSET": "Set the value of an element in a list by its index", "LTRIM": "Trim a list to the specified range", "MEMORY DOCTOR": "Outputs memory problems report", "MEMORY MALLOC-STATS": "Show allocator internal stats", "MEMORY PURGE": "Ask the allocator to release memory", "MEMORY STATS": "Show memory usage details", "MEMORY USAGE": "Estimate the memory usage of a key", "MGET": "Get the values of all the given keys", "MIGRATE": "Atomically transfer a key from a Redis instance to another one.", "MODULE LIST": "List all modules loaded by the server", "MODULE LOAD": "Load a module", "MODULE LOADEX": "Load a module with extended parameters", "MODULE UNLOAD": "Unload a module", "MONITOR": "Listen for all requests received by the server in real time", "MOVE": "Move a key to another database", "MSET": "Set multiple keys to multiple values", "MSETNX": "Set multiple keys to multiple values, only if none of the keys exist", "MULTI": "Mark the start of a transaction block", "OBJECT ENCODING": "Inspect the internal encoding of a Redis object", "OBJECT FREQ": "Get the logarithmic access frequency counter of a Redis object", "OBJECT IDLETIME": "Get the time since a Redis object was last accessed", "OBJECT REFCOUNT": "Get the number of references to the value of the key", "PERSIST": "Remove the expiration from a key", "PEXPIRE": "Set a key\'s time to live in milliseconds", "PEXPIREAT": "Set the expiration for a key as a UNIX timestamp specified in milliseconds", "PEXPIRETIME": "Get the expiration Unix timestamp for a key in milliseconds", "PFADD": "Adds the specified elements to the specified HyperLogLog.", "PFCOUNT": "Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).", "PFDEBUG": "Internal commands for debugging HyperLogLog values", "PFMERGE": "Merge N different HyperLogLogs into a single one.", "PFSELFTEST": "An internal command for testing HyperLogLog values", "PING": "Ping the server", "PSETEX": "Set the value and expiration in milliseconds of a key", "PSUBSCRIBE": "Listen for messages published to channels matching the given patterns", "PSYNC": "Internal command used for replication", "PTTL": "Get the time to live for a key in milliseconds", "PUBLISH": "Post a message to a channel", "PUBSUB CHANNELS": "List active channels", "PUBSUB NUMPAT": "Get the count of unique patterns pattern subscriptions", "PUBSUB NUMSUB": "Get the count of subscribers for channels", "PUBSUB SHARDCHANNELS": "List active shard channels", "PUBSUB SHARDNUMSUB": "Get the count of subscribers for shard channels", "PUNSUBSCRIBE": "Stop listening for messages posted to channels matching the given patterns", "QUIT": "Close the connection", "RANDOMKEY": "Return a random key from the keyspace", "READONLY": "Enables read queries for a connection to a cluster replica node", "READWRITE": "Disables read queries for a connection to a cluster replica node", "RENAME": "Rename a key", "RENAMENX": "Rename a key, only if the new key does not exist", "REPLCONF": "An internal command for configuring the replication stream", "REPLICAOF": "Make the server a replica of another instance, or promote it as master.", "RESET": "Reset the connection", "RESTORE": "Create a key using the provided serialized value, previously obtained using DUMP.", "RESTORE-ASKING": "An internal command for migrating keys in a cluster", "ROLE": "Return the role of the instance in the context of replication", "RPOP": "Remove and get the last elements in a list", "RPOPLPUSH": "Remove the last element in a list, prepend it to another list and return it", "RPUSH": "Append one or multiple elements to a list", "RPUSHX": "Append an element to a list, only if the list exists", "SADD": "Add one or more members to a set", "SAVE": "Synchronously save the dataset to disk", "SCAN": "Incrementally iterate the keys space", "SCARD": "Get the number of members in a set", "SCRIPT DEBUG": "Set the debug mode for executed scripts.", "SCRIPT EXISTS": "Check existence of scripts in the script cache.", "SCRIPT FLUSH": "Remove all the scripts from the script cache.", "SCRIPT KILL": "Kill the script currently in execution.", "SCRIPT LOAD": "Load the specified Lua script into the script cache.", "SDIFF": "Subtract multiple sets", "SDIFFSTORE": "Subtract multiple sets and store the resulting set in a key", "SELECT": "Change the selected database for the current connection", "SET": "Set the string value of a key", "SETBIT": "Sets or clears the bit at offset in the string value stored at key", "SETEX": "Set the value and expiration of a key", "SETNX": "Set the value of a key, only if the key does not exist", "SETRANGE": "Overwrite part of a string at key starting at the specified offset", "SHUTDOWN": "Synchronously save the dataset to disk and then shut down the server", "SINTER": "Intersect multiple sets", "SINTERCARD": "Intersect multiple sets and return the cardinality of the result", "SINTERSTORE": "Intersect multiple sets and store the resulting set in a key", "SISMEMBER": "Determine if a given value is a member of a set", "SLAVEOF": "Make the server a replica of another instance, or promote it as master.", "SLOWLOG GET": "Get the slow log\'s entries", "SLOWLOG LEN": "Get the slow log\'s length", "SLOWLOG RESET": "Clear all entries from the slow log", "SMEMBERS": "Get all the members in a set", "SMISMEMBER": "Returns the membership associated with the given elements for a set", "SMOVE": "Move a member from one set to another", "SORT": "Sort the elements in a list, set or sorted set", "SORT_RO": "Sort the elements in a list, set or sorted set. Read-only variant of SORT.", "SPOP": "Remove and return one or multiple random members from a set", "SPUBLISH": "Post a message to a shard channel", "SRANDMEMBER": "Get one or multiple random members from a set", "SREM": "Remove one or more members from a set", "SSCAN": "Incrementally iterate Set elements", "SSUBSCRIBE": "Listen for messages published to the given shard channels", "STRLEN": "Get the length of the value stored in a key", "SUBSCRIBE": "Listen for messages published to the given channels", "SUBSTR": "Get a substring of the string stored at a key", "SUNION": "Add multiple sets", "SUNIONSTORE": "Add multiple sets and store the resulting set in a key", "SUNSUBSCRIBE": "Stop listening for messages posted to the given shard channels", "SWAPDB": "Swaps two Redis databases", "SYNC": "Internal command used for replication", "TDIGEST.ADD": "Adds one or more observations to a t-digest sketch", "TDIGEST.BYRANK": "Returns, for each input rank, an estimation of the value (floating-point) with that rank", "TDIGEST.BYREVRANK": "Returns, for each input reverse rank, an estimation of the value (floating-point) with that reverse rank", "TDIGEST.CDF": "Returns, for each input value, an estimation of the fraction (floating-point) of (observations smaller than the given value + half the observations equal to the given value)", "TDIGEST.CREATE": "Allocates memory and initializes a new t-digest sketch", "TDIGEST.INFO": "Returns information and statistics about a t-digest sketch", "TDIGEST.MAX": "Returns the maximum observation value from a t-digest sketch", "TDIGEST.MERGE": "Merges multiple t-digest sketches into a single sketch", "TDIGEST.MIN": "Returns the minimum observation value from a t-digest sketch", "TDIGEST.QUANTILE": "Returns, for each input fraction, an estimation of the value (floating point) that is smaller than the given fraction of observations", "TDIGEST.RANK": "Returns, for each input value (floating-point), the estimated rank of the value (the number of observations in the sketch that are smaller than the value + half the number of observations that are equal to the value)", "TDIGEST.RESET": "Resets a t-digest sketch: empty the sketch and re-initializes it.", "TDIGEST.REVRANK": "Returns, for each input value (floating-point), the estimated reverse rank of the value (the number of observations in the sketch that are larger than the value + half the number of observations that are equal to the value)", "TDIGEST.TRIMMED_MEAN": "Returns an estimation of the mean value from the sketch, excluding observation values outside the low and high cutoff quantiles", "TIME": "Return the current server time", "TOPK.ADD": "Increases the count of one or more items by increment", "TOPK.COUNT": "Return the count for one or more items are in a sketch", "TOPK.INCRBY": "Increases the count of one or more items by increment", "TOPK.INFO": "Returns information about a sketch", "TOPK.LIST": "Return full list of items in Top K list", "TOPK.QUERY": "Checks whether one or more items are in a sketch", "TOPK.RESERVE": "Initializes a TopK with specified parameters", "TOUCH": "Alters the last access time of a key(s). Returns the number of existing keys specified.", "TS.ADD": "Append a sample to a time series", "TS.ALTER": "Update the retention, chunk size, duplicate policy, and labels of an existing time series", "TS.CREATE": "Create a new time series", "TS.CREATERULE": "Create a compaction rule", "TS.DECRBY": "Decrease the value of the sample with the maximal existing timestamp, or create a new sample with a value equal to the value of the sample with the maximal existing timestamp with a given decrement", "TS.DEL": "Delete all samples between two timestamps for a given time series", "TS.DELETERULE": "Delete a compaction rule", "TS.GET": "Get the last sample", "TS.INCRBY": "Increase the value of the sample with the maximal existing timestamp, or create a new sample with a value equal to the value of the sample with the maximal existing timestamp with a given increment", "TS.INFO": "Returns information and statistics for a time series", "TS.MADD": "Append new samples to one or more time series", "TS.MGET": "Get the last samples matching a specific filter", "TS.MRANGE": "Query a range across multiple time series by filters in forward direction", "TS.MREVRANGE": "Query a range across multiple time-series by filters in reverse direction", "TS.QUERYINDEX": "Get all time series keys matching a filter list", "TS.RANGE": "Query a range in forward direction", "TS.REVRANGE": "Query a range in reverse direction", "TTL": "Get the time to live for a key in seconds", "TYPE": "Determine the type stored at key", "UNLINK": "Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking.", "UNSUBSCRIBE": "Stop listening for messages posted to the given channels", "UNWATCH": "Forget about all watched keys", "WAIT": "Wait for the synchronous replication of all the write commands sent in the context of the current connection", "WATCH": "Watch the given keys to determine execution of the MULTI/EXEC block", "XACK": "Marks a pending message as correctly processed, effectively removing it from the pending entries list of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, the IDs we were actually able to resolve in the PEL.", "XADD": "Appends a new entry to a stream", "XAUTOCLAIM": "Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to the specified consumer.", "XCLAIM": "Changes (or acquires) ownership of a message in a consumer group, as if the message was delivered to the specified consumer.", "XDEL": "Removes the specified entries from the stream. Returns the number of items actually deleted, that may be different from the number of IDs passed in case certain IDs do not exist.", "XGROUP CREATE": "Create a consumer group.", "XGROUP CREATECONSUMER": "Create a consumer in a consumer group.", "XGROUP DELCONSUMER": "Delete a consumer from a consumer group.", "XGROUP DESTROY": "Destroy a consumer group.", "XGROUP SETID": "Set a consumer group to an arbitrary last delivered ID value.", "XINFO CONSUMERS": "List the consumers in a consumer group", "XINFO GROUPS": "List the consumer groups of a stream", "XINFO STREAM": "Get information about a stream", "XLEN": "Return the number of entries in a stream", "XPENDING": "Return information and entries from a stream consumer group pending entries list, that are messages fetched but never acknowledged.", "XRANGE": "Return a range of elements in a stream, with IDs matching the specified IDs interval", "XREAD": "Return never seen elements in multiple streams, with IDs greater than the ones reported by the caller for each stream. Can block.", "XREADGROUP": "Return new entries from a stream using a consumer group, or access the history of the pending entries for a given consumer. Can block.", "XREVRANGE": "Return a range of elements in a stream, with IDs matching the specified IDs interval, in reverse order (from greater to smaller IDs) compared to XRANGE", "XSETID": "An internal command for replicating stream values", "XTRIM": "Trims the stream to (approximately if \'~\' is passed) a certain size", "ZADD": "Add one or more members to a sorted set, or update its score if it already exists", "ZCARD": "Get the number of members in a sorted set", "ZCOUNT": "Count the members in a sorted set with scores within the given values", "ZDIFF": "Subtract multiple sorted sets", "ZDIFFSTORE": "Subtract multiple sorted sets and store the resulting sorted set in a new key", "ZINCRBY": "Increment the score of a member in a sorted set", "ZINTER": "Intersect multiple sorted sets", "ZINTERCARD": "Intersect multiple sorted sets and return the cardinality of the result", "ZINTERSTORE": "Intersect multiple sorted sets and store the resulting sorted set in a new key", "ZLEXCOUNT": "Count the number of members in a sorted set between a given lexicographical range", "ZMPOP": "Remove and return members with scores in a sorted set", "ZMSCORE": "Get the score associated with the given members in a sorted set", "ZPOPMAX": "Remove and return members with the highest scores in a sorted set", "ZPOPMIN": "Remove and return members with the lowest scores in a sorted set", "ZRANDMEMBER": "Get one or multiple random elements from a sorted set", "ZRANGE": "Return a range of members in a sorted set", "ZRANGEBYLEX": "Return a range of members in a sorted set, by lexicographical range", "ZRANGEBYSCORE": "Return a range of members in a sorted set, by score", "ZRANGESTORE": "Store a range of members from sorted set into another key", "ZRANK": "Determine the index of a member in a sorted set", "ZREM": "Remove one or more members from a sorted set", "ZREMRANGEBYLEX": "Remove all members in a sorted set between the given lexicographical range", "ZREMRANGEBYRANK": "Remove all members in a sorted set within the given indexes", "ZREMRANGEBYSCORE": "Remove all members in a sorted set within the given scores", "ZREVRANGE": "Return a range of members in a sorted set, by index, with scores ordered from high to low", "ZREVRANGEBYLEX": "Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.", "ZREVRANGEBYSCORE": "Return a range of members in a sorted set, by score, with scores ordered from high to low", "ZREVRANK": "Determine the index of a member in a sorted set, with scores ordered from high to low", "ZSCAN": "Incrementally iterate sorted sets elements and associated scores", "ZSCORE": "Get the score associated with the given member in a sorted set", "ZUNION": "Add multiple sorted sets", "ZUNIONSTORE": "Add multiple sorted sets and store the resulting sorted set in a new key"}')

    def __init__(self):
        self.start_command = True

    def can_handle(self, msg: dict, *args, **kwargs):
        msg_type = msg.get('type')
        return msg_type in ["FriendMessage", "GroupMessage"]

    def handle(self, msg: dict, ctx, *args, **kwargs):
        source_msg_chain = msg['messageChain']
        # Source类型永远为chain的第一个元素: {'type': 'Source', 'id': 26675, 'time': 1661589588}
        source_msg = source_msg_chain[0]
        logger.info(source_msg)
        cfg = ctx.cfg
        for single_msg in source_msg_chain:
            single_msg_type = single_msg['type']
            if single_msg_type == 'Plain':
                content: str = single_msg['text'].strip().lower()
                if ":redis" == content:
                    self.start_command = True
                    msg_chain = [
                        {"type": "Plain", "text": "已开启redis指令模式\n"},
                        {"type": "Plain", "text": "可以愉快的使用redis命令了！"},
                    ]
                    self._send(cfg, msg, msg_chain)
                elif ":redis:exit" == content:
                    self.start_command = False
                    msg_chain = [
                        {"type": "Plain", "text": "已关闭redis指令模式"},
                    ]
                    self._send(cfg, msg, msg_chain)
                elif ":redis:state" == content:
                    msg_chain = [
                        {"type": "Plain", "text": f"{self.start_command}"},
                    ]
                    self._send(cfg, msg, msg_chain)
                elif "commands" == content:
                    msg_chain = [
                        {"type": "Plain", "text": json.dumps(self.ALL_COMMANDS, indent=4)},
                    ]
                    self._send(cfg, msg, msg_chain)
                elif "desc" in content:
                    msg_chain = [
                        {"type": "Plain",
                         "text": self.CMD2DESC[" ".join(content.split()[1:]).upper()]},
                    ]
                    self._send(cfg, msg, msg_chain)
                else:
                    try:
                        if len(content.strip()) < 1:
                            return None
                        if self.start_command and content.split()[0] in self.ALL_COMMANDS:
                            if "flush" not in content \
                                    and "shutdown" not in content:
                                msg_chain = [
                                    {"type": "Plain", "text": f"redis> {content}\n\n"},
                                    {"type": "Plain", "text": str(util.exec_redis_cmd(content))},
                                ]
                                self._send(cfg, msg, msg_chain)
                            else:
                                msg_chain = [
                                    {"type": "Plain", "text": f"redis: 你想干嘛！？"},
                                ]
                                self._send(cfg, msg, msg_chain)
                    except Exception as e:
                        logger.exception(e)
                        msg_chain = [
                            {"type": "Plain", "text": str(e)},
                        ]
                        self._send(cfg, msg, msg_chain)

    def _send(self, cfg, msg, msg_chain):
        if msg["type"] == "FriendMessage":
            msg_send_utils.send_friend_msg(cfg["session"], get_sender_id(msg), msg_chain)
        else:
            send_group_msg(cfg["session"], get_group_id(msg), msg_chain)


class Card(object):

    def __init__(self, code: str, val: int = None, ctp=None):
        """
        牌
        :param code: 编码
        :param val: 值
        :param ctp: 类型：红桃，黑桃，梅花，方块
        """

        code = code.lower()
        self.code = code
        self._value = val or CODE2VAL[code]
        self.ctp = ctp

    def __lt__(self, other):
        return self.value < other.value

    def __gt__(self, other):
        return other < self

    def __eq__(self, other):
        return self.value == other.value

    def __repr__(self):
        return self.code

    def __sub__(self, other):
        return self.value - other.value

    def __add__(self, other):
        return self.value + other.value

    __str__ = __repr__

    @property
    def value(self) -> int:
        return self._value

    @value.setter
    def value(self, val):
        self._value = val


CODE2CARD = {}
for code, val in CODE2VAL.items():
    CODE2CARD[code] = Card(code=code, val=val)


def str2cards(cards_str: str) -> list[Card]:
    """
    字符串转卡牌
    :param cards_str: 卡牌代码字符串
    :return: 卡牌列表
    """
    return util.map_list(lambda card_code: CODE2CARD[card_code], cards_str.lower())


def cards2str(cards: list[Card]) -> str:
    """
    卡牌转字符串
    :param cards: 卡牌列表
    :return: 卡牌代码字符串
    """
    return util.map_list(str, cards)


def sort_cards_str(cards_str: str, reverse: bool = True) -> str:
    '''
    排序
    :param cards_str: 卡牌列表
    :return: 排序后的卡牌代码字符串
    '''

    cards: list[Card] = str2cards(cards_str.lower())
    return "".join(
        cards2str(sorted(cards, key=lambda c: c.value, reverse=reverse))
    ).upper()


DECK_CARDS: list[Card] = []

for value, code in enumerate("34567890jqka2"):
    for ctp in [1, 2, 3, 4]:
        DECK_CARDS.append(Card(code, ctp))

DECK_CARDS.append(Card('x'))
DECK_CARDS.append(Card('y'))

DEBUG = DEV

TYPE_DELIMITER = "@"

TYPE_RANGE = TYPE_DELIMITER + '["%s","%s"]'
TYPE_SINGLE = TYPE_DELIMITER + '"%s"'

TYPE_X = "单张" + TYPE_SINGLE
TYPE_MX = "顺子" + TYPE_RANGE

TYPE_XX = "对子" + TYPE_SINGLE
TYPE_MXX = "连对" + TYPE_RANGE
TYPE_BOOM = "王炸"

TYPE_XXX = "三个" + TYPE_SINGLE
TYPE_MXXX = "连三个" + TYPE_RANGE
TYPE_XXX_Y = "三带一" + TYPE_SINGLE
TYPE_XXX_YY = "三带二" + TYPE_SINGLE
TYPE_PLAIN_1 = "飞机-1" + TYPE_RANGE
TYPE_PLAIN_2 = "飞机-2" + TYPE_RANGE

TYPE_XXXX = "炸弹" + TYPE_SINGLE

TYPE_XXXX_N = "四带二-%s" + TYPE_SINGLE


class FightingTheLandlordMsgHandler(SingleMsgHandlerBase):
    """
    斗地主 - 基于 Redis
    """

    # 命令
    CMD_INIT_GAME: list = ["开始游戏", "创建游戏", "启动游戏"]
    CMD_HANDOUT_CARDS: list = ["发牌", "发牌啊"]
    CMD_LANDLORD: list = ["抢", "不抢", "抢地主"]
    CMD_PLAY_CARDS: re.Pattern = re.compile(r"([0-9jqkaxy]+)")
    CMD_PASS: list = ["不要", "过", "要不起", "pass"]
    CMD_JOIN: list = ["加入", "加入游戏"]
    CMD_END_GAME: list = ["结束游戏", "投降", "强制结束游戏"]

    # 状态
    ST_STAGE = "game_stage"
    STATE_TIPS_MAP = {
        0: "还未创建游戏",
        1: "组队阶段",
        2: "抢地主阶段",
        3: "出牌阶段",
    }

    # 常量
    CST_TABLE = "table"
    CST_CARDS = "cards"
    CST_PLAY_INDEX = "play_index"
    CST_LANDLORD = "landlord"
    CST_LANDLORD_CARD = "landlord_card"
    CST_DESK_TOP = "desk_top"

    def __init__(self, host=None, port=None, db=None):
        import redis
        host = host or cfg["redis"].get("host")
        port = port or cfg["redis"].get("port")
        db = db or cfg["redis"].get("db")
        redis_pool = redis.ConnectionPool(host=host, port=port, db=db)
        self.redis_cli = redis.Redis(connection_pool=redis_pool)
        self.turn_on = True

    def can_handle(self, msg: dict, *args, **kwargs):
        msg_type = msg.get('type')
        return msg_type in ["GroupMessage"]

    def handle(self, msg: dict, ctx, *args, **kwargs):
        source_msg_chain = msg['messageChain']
        # Source类型永远为chain的第一个元素: {'type': 'Source', 'id': 26675, 'time': 1661589588}
        source_msg = source_msg_chain[0]
        logger.info(source_msg)
        cfg = ctx.cfg
        for single_msg in source_msg_chain:
            single_msg_type = single_msg['type']
            if single_msg_type == 'Plain':
                content: str = single_msg['text'].strip().lower()
                self._do_special_cmd(content, msg)
                if self.turn_on:
                    self._do_core(content, msg)

    def _do_core(self, content, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        if content in ["游戏状态", "当前游戏状态"]:
            return self._send_stage_info(msg, prefix)

        # 0. 创建游戏
        if not self._stage(msg):
            # 创建游戏
            if content in self.CMD_INIT_GAME:
                self._do_init(content, msg)
                self._incr_stage(msg)
                return None

        # 1. 组队阶段
        elif self._stage(msg) == 1:
            # 加入游戏
            if content in self.CMD_JOIN:
                self._do_join(content, msg)
                # 可能会开始抢地主
                self._maybe_landlord(content, msg)
                return None

        # 2. 抢地主阶段
        elif self._check_identity(content, msg) and self._stage(msg) == 2:
            self._handout_card_one(content, msg)
            # 抢地主
            if content in self.CMD_LANDLORD:
                self._do_landlord(content, msg)
                if self._has_landlord(content, msg) is True:
                    self._incr_stage(msg)

        # 3. 出牌阶段
        elif self._check_identity(content, msg) and self._stage(msg) == 3:
            self._handout_card_one(content, msg)
            matcher = self.CMD_PLAY_CARDS.match(content)
            if matcher:
                self._do_play_cards(matcher.group(1), msg)
                self._check_cards_num(msg)
            # 不要
            elif content in self.CMD_PASS:
                self._do_pass(content, msg)
            # 结束游戏 | 投降
            elif content in self.CMD_END_GAME:
                self._do_end_game(msg)
        # 4. 清算阶段
        pass

    def _send_stage_info(self, msg, prefix):
        stage = self._stage(msg)
        self._send(msg, self.STATE_TIPS_MAP[stage] + " " + str(
            util.map_list(int, self.redis_cli.smembers(f"{prefix}:players"))))
        return None

    def _handout_card_one(self, content, msg):
        # 发牌
        if content in self.CMD_HANDOUT_CARDS:
            self._do_handout_cards(content, msg)

    def _do_init(self, content, msg):
        if self._has_creat(content, msg):
            self._send(msg, "已经有一个游戏在运行")
        else:
            self._creat_game(content, msg)

    def _do_join(self, content, msg):
        if not self._has_creat(content, msg):
            self._send(msg, "还没创建游戏")
        elif self._has_start(content, msg):
            self._send(msg, "游戏已经开始了")
        else:
            self._join(content, msg)

    def _do_landlord(self, content, msg):
        self._landlord(content, msg)

    def _do_handout_cards(self, content: str = None, msg: dict = None, group=None):
        if group is not None:
            self._send_card_three(content, msg)
        else:
            self._send_card_one(content, msg)

    def _do_play_cards(self, content, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        if self._is_his_time(content, msg):
            cur_cards_type: str = self._get_cards_type(content)
            can_play = False
            pre_cards_type, pre_uid, pre_cards_str = self._desk_top(msg)
            if pre_uid == get_sender_id(msg) \
                    or pre_uid == -1:
                can_play = True
            elif cur_cards_type:
                can_play = self._check_played_cards_valid(pre_cards_type, cur_cards_type)

            if can_play:
                self._remove_played_cards(msg, content)
                self._send_friend_msg(get_sender_id(msg), "这是你剩下的牌：\n" + self._get_cards(msg))
                self._incr_play_index(msg, prefix)
                self._set_desk_top(cur_cards_type, msg, content)
                self._send_at(msg, self._next_player(prefix), f"该你了\n这是刚才出的牌：{sort_cards_str(content)}")
            else:
                self._send_at(msg, get_sender_id(msg), "你出的牌不对， 请检查一下")

        else:
            self._send_at(msg, get_sender_id(msg), "还没轮到你出牌呢")

    def _incr_play_index(self, msg, prefix):
        self.redis_cli.hset(prefix, self.CST_PLAY_INDEX, (self._play_index(msg) + 1) % 3)

    def _do_pass(self, content, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        pre_cards_type, pre_uid, pre_cards_str = self._desk_top(msg)
        if str(pre_uid) == get_sender_id(msg) or pre_uid == -1:
            self._send(msg, f"你可以随便出， 你 `{content}` 是个什么鬼...")
            return None
        self._incr_play_index(msg, prefix)
        self._send_at(msg, self._next_player(prefix), f"该你了\n这是刚才出的牌：{sort_cards_str(pre_cards_str)}")

    def _do_end_game(self, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        loser = get_sender_id(msg)
        landlord = int(self.redis_cli.hset(prefix, self.CST_LANDLORD))
        if loser != landlord:
            self._end_game(msg, winner=landlord)
        else:
            players: list = util.map_list(int, self.redis_cli.smembers(f"{prefix}:players"))
            for player in players:
                if player != loser:
                    self._end_game(msg, winner=player)
                    break

    def _add_loser_tip(self, msg_chain, player):
        msg_chain.extend([
            {"type": "At", "target": player},
            {"type": "Plain", "text": " "},
            {"type": "Face", "target": 9},
        ])

    def _add_winner_tip(self, msg_chain, player):
        msg_chain.extend([
            {"type": "At", "target": player},
            {"type": "Plain", "text": " "},
            {"type": "Face", "target": 4},
        ])

    def _has_creat(self, content, msg):
        return (self._stage(msg) or 0) > 0

    def _incr_stage(self, msg):
        self.redis_cli.hincrby(f"{self.CST_TABLE}:{get_group_id(msg)}", self.ST_STAGE)

    def _send(self, msg, send_msg, fun=lambda x: x):
        if isinstance(send_msg, list):
            msg_chain = send_msg
        else:
            send_msg = send_msg.decode("utf-8") if isinstance(send_msg, (bytes, bytearray)) else send_msg
            msg_chain = [
                {"type": "Plain", "text": fun(send_msg)}
            ]
        if DEBUG:
            for it in msg_chain:
                it.update(target=1109529495)
            send_group_msg(cfg["session"], 725299875, msg_chain)
            return None
        send_group_msg(cfg["session"], get_group_id(msg), msg_chain)

    def _has_start(self, content, msg):
        return self._stage(msg) > 1

    def _creat_game(self, content, msg):
        # 打乱牌
        random.shuffle(DECK_CARDS)
        self.redis_cli.hset(f"{self.CST_TABLE}:{get_group_id(msg)}", self.CST_CARDS, "".join(map(str, DECK_CARDS)))
        self.redis_cli.hset(f"{self.CST_TABLE}:{get_group_id(msg)}", self.ST_STAGE, 0)
        self._send(msg, "游戏创建成功！ 请加入游戏...")

    def _join(self, content, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"

        if self.redis_cli.scard(f"{prefix}:players") == 3:
            self._send(msg, "人数已满，无法再加入游戏")
            return None

        join_game = self.redis_cli.sadd(f"{prefix}:players", get_sender_id(msg))
        to_send = "加入成功" if join_game else "加入失败"
        to_send += " 当前玩家：" + str(util.map_list(int, self.redis_cli.smembers(f"{prefix}:players")))
        self._send(msg, to_send)

    def _send_at(self, msg, target, send_msg, fun=lambda x: x):
        group_id = get_group_id(msg)
        send_msg = send_msg.decode("utf-8") if isinstance(send_msg, (bytes, bytearray)) else send_msg

        if DEBUG:
            group_id = 725299875
            target = 1109529495
        msg_chain = [
            {"type": "At", "target": int(target)},
            {"type": "Plain", "text": " " + fun(send_msg)},
        ]
        send_group_msg(cfg["session"], group_id, msg_chain)

    def _landlord(self, content: str, msg: dict):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        cur_index = self._play_index(msg)
        if self._is_his_time(content, msg):
            if content.startswith("抢"):
                self.redis_cli.hset(prefix, self.CST_LANDLORD, get_sender_id(msg))
            elif content.startswith("不抢"):
                self.redis_cli.hset(prefix, self.CST_PLAY_INDEX, (cur_index + 1) % 3)
                self._send_next_landlord(content, msg)
        else:
            self._send(msg, "还没到你抢地主呢！")

    def _check_identity(self, content, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        return self.redis_cli.sismember(f"{prefix}:players", get_sender_id(msg))

    def _maybe_landlord(self, content, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        if self.redis_cli.scard(f"{prefix}:players") == 3 and not self._has_start(content, msg):
            # 发牌
            self._do_handout_cards(msg=msg, group=True)
            self._incr_stage(msg)

            players: set = self.redis_cli.smembers(f"{prefix}:players")
            assert isinstance(players, set)
            # 初始化抢地主
            for i in range(3):
                self.redis_cli.hset(prefix,
                                    f"player{i}",
                                    players.pop())

            self._send_at(msg, self.redis_cli.hget(prefix, "player0"), "开始抢地主吧")
            # 设置说话顺序
            self.redis_cli.hset(prefix, self.CST_PLAY_INDEX, 0)

    def _is_his_time(self, content: str, msg: dict):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        ind = int(self.redis_cli.hget(prefix, self.CST_PLAY_INDEX) or 0)
        return int(self.redis_cli.hget(prefix, f"player{ind}") or 0) == int(get_sender_id(msg))

    def _send_next_landlord(self, content, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        target = self._next_player(prefix)
        self._send_at(msg, target, "到你抢地主了")

    def _next_player(self, prefix):
        ind = self.redis_cli.hget(prefix, self.CST_PLAY_INDEX)
        target = self.redis_cli.hget(prefix, f"player{ind}")
        return target

    def _has_landlord(self, content, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        if self._landlord_id(prefix) == get_sender_id(msg):
            card = self.redis_cli.hget(prefix, f"card:{get_sender_id(msg)}")
            landlord_card = self.redis_cli.hget(prefix, self.CST_LANDLORD_CARD)
            self.redis_cli.hset(prefix, f"card:{get_sender_id(msg)}", card + landlord_card)
            self._send_friend_msg(get_sender_id(msg),
                                  self.redis_cli.hget(prefix, f"card:{get_sender_id(msg)}"),
                                  fun=sort_cards_str)
            self._send_at(msg, get_sender_id(msg), "抢到地主了！")
            self._send(msg, "地主牌为：" + sort_cards_str(landlord_card.decode()))
            self._send(msg, "进入出牌阶段...")
            return True

    def _landlord_id(self, prefix):
        return int(self.redis_cli.hget(prefix, self.CST_LANDLORD) or 0)

    def _send_card_three(self, content, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        players: set = self.redis_cli.smembers(f"{prefix}:players")
        assert isinstance(players, set)
        cards: str = self.redis_cli.hget(prefix, self.CST_CARDS).decode()

        for player in players:
            player = player.decode() if isinstance(player, bytes) else player
            self.redis_cli.hset(prefix, f"card:{player}", sort_cards_str(cards[:17]))
            self._send_friend_msg(player, cards[:17], fun=sort_cards_str)
            self._send_at(msg, player, "你的牌发送完成")
            cards = cards[17:]
        assert len(cards) == 3, "地主的牌应该有3张"
        self.redis_cli.hset(prefix, self.CST_LANDLORD_CARD, cards)

    def _send_card_one(self, content, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        target = get_sender_id(msg)
        card = self.redis_cli.hget(prefix, f"card:{target}")
        self._send_friend_msg(target, card, fun=sort_cards_str)
        self._send_at(msg, target, "你的牌发送完成")

    def _send_friend_msg(self, target, send_msg: Union[bytes, str], fun=lambda x: x):
        send_msg = send_msg.decode("utf-8") if isinstance(send_msg, (bytes, bytearray)) else send_msg

        if DEBUG:
            target = 1109529495

        msg_chain = [
            {"type": "Plain", "text": " " + fun(send_msg)},
        ]
        send_friend_msg(cfg["session"], target, msg_chain)

    def _stage(self, msg):
        return int(self.redis_cli.hget(f"{self.CST_TABLE}:{get_group_id(msg)}", self.ST_STAGE) or 0)

    def _clear(self, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        delete = False
        for key in self.redis_cli.keys(f"{prefix}*"):
            delete = self.redis_cli.delete(key)
        if delete:
            self._send(msg, prefix + " 数据清理完毕！")

    def _play_index(self, msg):
        return int(self.redis_cli.hget(f"{self.CST_TABLE}:{get_group_id(msg)}", self.CST_PLAY_INDEX) or 0)

    def _get_cards_type(self, content: str) -> Union[str, None]:
        cards: list[Card] = str2cards(content)
        cards_vals: list[int] = util.map_list(lambda _card: _card.value, cards)
        c = Counter(content)
        code, num = c.most_common(1)[0]
        if num == 1:
            if len(content) == 1:
                return TYPE_X % code
            # 王炸
            elif len(content) == 2 and \
                    any(['xy' == content.lower(), 'yx' == content.lower()]):
                return TYPE_BOOM
            # 顺子
            elif len(content) >= 5:
                if len(content) == max(cards_vals) - min(cards_vals) + 1:
                    return TYPE_MX % (min(cards_vals), max(cards_vals))

        elif num == 2:
            if len(c) == 1:
                return TYPE_XX % code
            elif len(c) >= 3 \
                    and all(util.map_list(lambda it: it[1] == 2, c.items())):
                if len(c) == max(cards) - min(cards) + 1:
                    return TYPE_MXX % (min(cards), max(cards))

        elif num == 3:
            if len(c) == 1:
                # 三个
                return TYPE_XXX % code
            elif len(c) == 2:
                # 三带一/二
                c_values = c.values()
                assert 3 in c_values, "确定一下c_values 的数据类型"
                feature = max(
                    list(
                        map(
                            lambda it: CODE2CARD[it[0]],
                            util.filter_list(lambda it: it[1] == 3, c.items())
                        )
                    )
                )
                if 1 in c_values:
                    return TYPE_XXX_Y % feature
                elif 2 in c_values:
                    return TYPE_XXX_YY % feature
            elif len(c) >= 3 \
                    and all(util.map_list(lambda it: it[1] == 3, c.items())):
                # 连三个
                if len(c) == max(cards) - min(cards) + 1:
                    return TYPE_MXXX % (min(cards), max(cards))
            elif len(c) >= 4:
                # 飞机
                xxx, xx, x = util.group(
                    (lambda it: it[1] == 3,
                     lambda it: it[1] == 2,
                     lambda it: it[1] == 1,),
                    obj=c.items()
                )
                # 三个的必须连续
                _cards = util.map_list(lambda t: CODE2CARD[t[0]], xxx)
                feature = (min(_cards), max(_cards))
                if len(xxx) == max(_cards) - min(_cards) + 1:
                    _3, _2, _1 = len(xxx), len(xx), len(x)
                    if _3 == _2 and _1 == 0:
                        return TYPE_PLAIN_2 % feature
                    elif _3 == _1 and _2 == 0:
                        return TYPE_PLAIN_1 % feature
        elif num == 4:
            # 炸弹
            if len(c) == 1:
                return TYPE_XXXX % code
            # 四带二
            elif len(c) == 3:
                _, c1, c2 = c.most_common(3)
                if c1[-1] == c2[-1] and c1[-1] < 4:
                    return TYPE_XXXX_N % (c1[-1], code)
        return None

    def _desk_top(self, msg: dict) -> tuple[str, int, str]:
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        pre_cards_type, uid, cards_str = eval(self.redis_cli.hget(prefix, self.CST_DESK_TOP) or '["", -1, ""]')
        return pre_cards_type, uid, cards_str

    def _check_played_cards_valid(self, pre_type, cur_type):
        pre_prefix, pre_suffix = pre_type.split(TYPE_DELIMITER)
        cur_prefix, cur_suffix = cur_type.split(TYPE_DELIMITER)
        return pre_prefix == cur_prefix and self._compare(pre_suffix, cur_suffix)

    def _compare(self, pre_suffix, cur_suffix):
        pre = eval(pre_suffix)
        cur = eval(cur_suffix)
        if type(pre) != type(cur):
            return False
        else:
            if isinstance(pre, str):
                return CODE2CARD[cur] > CODE2CARD[pre]
            elif isinstance(pre, list):
                return len(pre) == len(cur) and CODE2CARD[cur[-1]] > CODE2CARD[pre[-1]]

    def _set_desk_top(self, cur_cards_type, msg, cards_str):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        return self.redis_cli.hset(
            prefix,
            self.CST_DESK_TOP,
            json.dumps(
                [cur_cards_type, get_sender_id(msg), sort_cards_str(cards_str)],
                ensure_ascii=False)
        )

    def _check_cards_num(self, msg):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        players: set = self.redis_cli.smembers(f"{prefix}:players")
        assert isinstance(players, set)
        to_send_msg = []
        for player in players:
            player = player.decode() if isinstance(player, bytes) else player
            cards_len = len(self._get_cards(msg, target=player))
            if cards_len <= 3:
                if cards_len == 0:
                    self._end_game(msg)
                to_send_msg.extend([
                    {"type": "Plain", "text": f"注意！注意！"},
                    {"type": "At", "target": int(player)},
                    {"type": "Plain", "text": f" 还剩 {cards_len} 张牌了\n"},
                ])
        if len(to_send_msg) > 0:
            self._send(msg, to_send_msg)

    def _remove_played_cards(self, msg, content):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        pre_cards = self._get_cards(msg, target=get_sender_id(msg))
        pre = list(pre_cards.lower())
        for ch in content:
            pre.remove(ch)
        self.redis_cli.hset(prefix, f"card:{get_sender_id(msg)}", sort_cards_str("".join(pre)))

    def _get_cards(self, msg, target=None):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        target = target or get_sender_id(msg)
        cards_str = self.redis_cli.hget(prefix, f"card:{target}")
        return cards_str.decode("utf-8") if isinstance(cards_str, bytes) else cards_str

    def _end_game(self, msg, winner=None):
        prefix = f"{self.CST_TABLE}:{get_group_id(msg)}"
        winner = winner or int(get_sender_id(msg))
        landlord = int(self.redis_cli.hset(prefix, self.CST_LANDLORD))
        is_landlord_win = winner == landlord
        msg_chain = [
            {"type": "Plain", "text": "地主胜利！\n" if is_landlord_win else "平民胜利！\n"},
            {"type": "Face", "target": 144},
            {"type": "Face", "target": 144},
            {"type": "Face", "target": 144},
        ]
        players: set = util.map_list(int, self.redis_cli.smembers(f"{prefix}:players"))
        for player in players:
            if is_landlord_win:
                if player == landlord:
                    self._add_winner_tip(msg_chain, player)
                else:
                    self._add_loser_tip(msg_chain, player)
            else:
                if player == landlord:
                    self._add_loser_tip(msg_chain, player)
                else:
                    self._add_winner_tip(msg_chain, player)
        self._clear(msg)

    def _is_root(self, send_id):
        return cfg["root"] == send_id

    def _do_special_cmd(self, content, msg):

        if self._is_root(get_sender_id(msg)):
            if content in self.CMD_END_GAME:
                self._clear(msg)
                return None
            elif content in ["关闭斗地主"]:
                self.turn_on = False
                self._send(msg, "已关闭斗地主")
            elif content in ["开启斗地主"]:
                self.turn_on = True
                self._send(msg, "已开启斗地主")


if __name__ == "__main__":
    def test_get_cards_type():
        tester = FightingTheLandlordMsgHandler("192.168.227.129", 6379, 1)
        print(tester._check_played_cards_valid('对子@"8"', '对子@"0"'))
        print(tester._check_played_cards_valid('对子@["8","0"]', '对子@["0","q"]'))
        print(tester._get_cards_type("000076"))
        print(tester._get_cards_type("00006699"))
        print(tester._get_cards_type("0000666999"))
        print(tester._get_cards_type("000066699"))


    test_get_cards_type()
